iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 7
0
自我挑戰組

邁向 JavaScript 核心之路 系列 第 7

[Day 7] JavaScript 語法特性 - 閉包 ( Closure )

  • 分享至 

  • xImage
  •  

閉包簡介

在說明閉包之前,如果對於範圍鏈 ( Scope chain ) 還不是很明白的朋友,建議先去複習昨天的文章或稍作了解後在看本篇會比較好理解哦!

閉包的概念為,一個擁有巢狀結構的函式,使其中內層函式去操作外層函式的區域變數,此時外層函式的變數狀態就會被內層保留下來,形成一個封閉的狀態,只有內層可以存取該變數,來達到保護變內容的目的。

閉包雖然很好用,但也有可能發生佔用記憶體的情況發生,所以在使用上必須謹慎小心,如果對於閉包還想瞭解更多的同學,我在文末有附上一些資料,可以參閱。

以下就讓我們用程式碼來說明吧!

閉包的應用

    // 基礎閉包運用

    var name = "金城武";
    var obj = {
        name: "郭富城",
        getName: function() { // 建立一個巢狀結構的函式
            return function() {
                return this.name;
            }
        }
    };
    
    // 當變數指向內部函式時,閉包就會形成。
    
    var result = obj.getName(); // 形成閉包
    console.log(result()); 
    

看到這邊,請各位思考一下最後一個 console 顯示出來的會是?

答案是 金城武。

因為 function 是在全域下執行的,所以這邊的 this 指的是全域 ( windows ),顯示出來的也就會是 金城武。
但這很明顯不符合我們的需求,還記得上面簡介所說的目的嗎?

使內層的函式去操作外層函式的區域變數,讓其形成一個封閉的狀態,只有內層可以存取該變數。
所以讓我們來看一下如何解決這個需求吧

    
    var name = "金城武";
    var obj = {
        name: "郭富城",
        getName: function() {
            var that = this;
            return function() {
                return that.name;
            }
        }
    };
    
    var result = obj.getName(); // 形成閉包
    console.log(result());
    

這邊的答案就會是 郭富城,因為我們在內層函式中定義了 that,藉此形成了閉包綁住 this,也因為如此才能順利的找到這個物件的 name 。

如果對於 this 不太了解的同學,別緊張,後續的章節我們還會針對 this 來做介紹。

接下來我們再用幾個程式碼來加深印象吧!


    function add() {
        var count = 0;
        return function() {
            return count += 1;
        }
    }

    var user = add(); // 形成閉包
    var custom = add(); // 形成閉包

    console.log("user", user()); // 1
    console.log("user", user()); // 2
    console.log("user", user()); // 3
    console.log("user", user()); // 4

    console.log("custom", custom()); // 1
    console.log("custom", custom()); // 2
    console.log("custom", custom()); // 3
    console.log("custom", custom()); // 4
    

這個案例當中,我們利用了閉包來做到了記憶變數,在這邊偷偷跟各位同學說個秘密,小弟以前在開發類似的功能,還會定義 user1、user2 ... 到user999來記憶不同的變數呢,但自從知道閉包後,不僅省時省力也讓程式碼更簡潔也更好維護了呢!

閉包雖然很好用,但很容易發生佔用記憶體的狀況,那我們該如何解決這樣的情況呢?

    var name = "金城武";
    var obj = {
        name: "郭富城",
        getName: function() {
            var that = this;
            return function() {
                return that.name;
            }
        }
    };
    
    var result = obj.getName(); // 當變數指向內部函式時,形成閉包
    console.log(result()); // 郭富城
    
    result = null; // 釋放閉包

是不是非常的簡單?只要將指向內部函式的變數設定為 null,即可釋放被佔用的記憶體。

這是因為 JavaScript 有所謂的回收機制,簡單來說,當一個變數已經不在被使用時,JavaScript 會將配置給該變數的記憶回收 (GC),如果有興趣想瞭解更多的同學,我在文末也會附上相關資料。

最後,我們來用一個大家日常中都遇到過的情況來結尾吧

    
    for( var i = 0; i < 5; i++ ) {
        setTimeout(function() {
            console.log(i);
        }, i * 1000);
    }
    

聰明的同學們,你們認為顯示出來的會是 A or B 呢?

(A) 0, 1, 2, 3, 4
(B) 5, 5, 5, 5, 5

答案是 (B),答對的同學非常棒,已經可以按下一篇了,
答錯的同學也別氣餒,知恥而近乎勇,讓我們來了解一下為什麼會是 B 吧!

因為該函式裡頭的 console 會存取的範圍為 for迴圈 所在的範圍( 目前是全域 ),因此在 1秒、2秒、3秒 ... 5秒後執行的 console.log(i),會去取全域變數,而這時 for 迴圈早已跑完,所以 i 也早就變成了 5,因此每過一秒後,印出來的都會是 i 的值,也就是 5 。

那知道了問題點,我們該怎麼解決呢?

沒錯,就是利用閉包能記憶變數這點來解決!


    // 利用閉包來解決
    
    for( var i = 0; i < 5; i++ ) {
        (function(i) { 
            setTimeout(function() { 
                console.log(i);
            }, i * 1000);
        })(i); // 利用立即函式來做出一個巢狀結構,並將 i 當作參數傳入函式內。
    }
    

因此,就能依照我們的先前所預期的,會印出 (A) 0, 1, 2, 3, 4。

此題也可以使用 ES6 的 let 或 bind 方法來解決此問題,以下提供程式碼來讓同學思考一下。


    // 使用 ES6 的 let 
    
    for( let i = 0; i < 5; i++ ) {
        setTimeout(function() {
            console.log(i);
        }, i * 1000);
    }
    

小提示, let 與 var 的作用域差異。


    // 使用 bind 方法 
    
    for ( var i = 0; i < 5; i++ ) {
      setTimeout(function() {
        console.log(this);
      }.bind(i), i * 1000);
    }
    

小提示, bind 能影響到 this。


寫這篇文章時,不時翻閱手邊的書籍與資料,在完成後再看閉包有種更清晰的感覺,所以說,寫文章就是行!

參考資料:

Tommy - 深入 JavaScript 核心課程
MDN - 閉包
MDN - 記憶體管理
MDN - Bind


上一篇
[Day 6] JavaScript 語法特性 - 範圍鏈 ( Scope chain )
下一篇
[Day 8] JavaScript 語法特性 - Currying
系列文
邁向 JavaScript 核心之路 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言